using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using System.Drawing;

namespace AutoFormatter
{

    #region code, the actual parsing of the RTF text

    class RtfConverter : CanAppend
    {
        /// <summary>
        /// rtf characters which are ignored
        /// </summary>
        private const string IgnoreList = "\r\n{}";
        /// <summary>
        /// The richt text formatted text
        /// </summary>
        string txt;
        /// <summary>
        /// The length of the rtf text
        /// </summary>
        int len;
        /// <summary>
        /// The current index of text
        /// </summary>
        int index;
        /// <summary>
        /// The available object that can parse rtf keywords to html tags
        /// </summary>
        List<HtmlType> htmltypes;

        public RtfConverter(string RTFText) : this(RTFText, 4, 20) { }
        public RtfConverter(string RTFText, int TabSpaces,int TabIndent)
        {
            txt = RTFText;
            len = txt.Length;
            this.TabSpaces = TabSpaces;
            this.TabIndent = TabIndent;
        }

        #region keywords

        /// <summary>
        /// Each character in this string can signify the end of a keyword
        /// </summary>
        const string EndOfKeyword = " {\\\r\n;";
        string SearchEndOfKeyword()
        {
            int kwstart = index;
            //search the end of the keyword
            while (++index < len && EndOfKeyword.IndexOf(txt[index]) == -1) { }
            return txt.Substring(kwstart, index - kwstart);
        }
        /// <summary>
        /// This method is called when an rtf keyword should be read in
        /// </summary>
        void ReadKeyWord()
        {            
            //get the keyword
            string kw = SearchEndOfKeyword();
            switch (kw)
            {
                case "par": //linefeed
                    tab.StartNewLine();
                    break;
                case "tab":
                    tab.Add(1);
                    break;
                case "colortbl": //information about the colours used in the rich text
                    ReadColourTable();
                    break;
                default:

                    if (kw.StartsWith(FontKeyword))
                    {
                        ReadFontTable(ref kw);
                    }
                    else
                    //possible unknown or unused keyword, if keyword
                    //is followed by {} or series of {{}} , proceed until after
                    //the keyword information
                    if (txt[index] == '{')
                    {
                        ReadToEndOfAccolade();
                    }
                    //check the default start stop keyword (such as bold, italic, etc)
                    //if the keyword signifies the start or stop of such as setting
                    else
                    {
                        foreach (HtmlType ht in htmltypes)
                        {
                            if (ht.CheckChanged(ref kw,s))
                            {
                                break;
                            }
                        }
                    }
                    break;
            }

            //the end of a keyword can be one of a number of characters,
            //but in case it is a \ , it can be the start of a new keyword
            //and has to be reset
            if (txt[index] == '\\') index--;

        }

        /// <summary>
        /// Reads to the ending accolade. Expected is, that the current index is
        /// positioned on the starting accolade (NB: this is not checked!)
        /// </summary>
        void ReadToEndOfAccolade()
        {
            int cnt = 1;
            for (index++; cnt > 0; index++)
            {
                if (txt[index] == '{') cnt++;
                else if (txt[index] == '}') cnt--;
            }
        }

        /// <summary>
        /// Reads out information about the colours used in the rich text
        /// </summary>
        void ReadColourTable()
        {
            List<Color> al = new List<Color>();
            index += 2;
            Regex rx = new Regex(@"\\red(\d+)\\green(\d+)\\blue(\d+)");
            int cstart;
            while (txt[index] != '}')
            {
                cstart = index;
                while (txt[++index] != ';') ;
                Match m = rx.Match(txt, cstart, index - cstart);
                if (!m.Success) throw new Exception("Unexpected error: Could not parse colors");
                al.Add(Color.FromArgb(
                    int.Parse(m.Groups[1].Value),
                    int.Parse(m.Groups[2].Value),
                    int.Parse(m.Groups[3].Value)));
                index++;
            }
            //surpass the last accolade
            index++;

            //add color html type
            htmltypes.Add(new FontColor(al)); 
        }


        const string FontKeyword = "deflang";
        /// <summary>
        /// Reads in information about the used font or fonts
        /// </summary>
        /// <param name="kw"></param>
        void ReadFontTable(ref string kw)
        {
            int Language = int.Parse(kw.Substring(FontKeyword.Length));
            
            int curindex = index;
            ReadToEndOfAccolade();
            
            Match ftbl= Regex.Match(
                txt.Substring(curindex, index - curindex),
                @"\\fonttbl({.+?})+");
            if (!ftbl.Success)throw new Exception("Could not succesfully parse font table information!");

            Regex rx = new Regex(@"\\f(?<nr>\d+).+?\\fcharset\d+ (?<fam>[^;]+)");
            FontType f = new FontType(Language);
            int fontindex=0;
            Group g = ftbl.Groups[1]; //second groups contains the enumeration
            {
                
                foreach(Capture c in g.Captures)
                {
                    Match fontinfo = rx.Match(c.Value);
                    if (!fontinfo.Success) throw new Exception("Could not parse font info: " + c.Value);
                    int nr = int.Parse(fontinfo.Groups["nr"].Value);
                    if (nr != fontindex++)
                        throw new Exception("Unexpected font order");

                    FontType.FontInfo fi = new FontType.FontInfo();
                    fi.FontFamily = fontinfo.Groups["fam"].Value;
                    f.AddInfo(fi);

                }
            }
            htmltypes.Add(f);
           
            
        }

        void CloseAllKeywords()
        {
            foreach (HtmlType ht in htmltypes)
            {
                ht.Close(s);
            }
        }
        #endregion

        #region Indentation (Tabber)
        /// <summary>
        /// The pixelwidth used for one tab
        /// </summary>
        int TabIndent;

        /// <summary>
        /// The object that keeps track of the indentations
        /// </summary>
        Tabber tab;

        /// <summary>
        /// Keeps track of the amount of tabs
        /// </summary>
        private class Tabber
        {
            private RtfConverter converter;
            StringBuilder s;
            private Stack<int> divs = new Stack<int>();
            public int OpenDivs { get { return divs.Count; } }

            public Tabber(RtfConverter converter)
            {
                this.converter = converter;
                s = converter.s;
            }
            public int PrevCount = 0;
            private int CurrentTabCount = 0;
            private bool newline = true;

            public void StartNewLine()
            {
                StopCount();
                newline = true;
                CurrentTabCount = 0;
            }
            /// <summary>
            /// Indicates that a newline is begun and the Tabber is still counting.
            /// Counting stops as soon as text begins (<see cref="StartText"/>
            /// </summary>
            public bool Counting
            {
                get { return newline; }
            }
            int linecount;
            public void StopCount()
            {
                if (!newline) return;
                if (linecount++ > 0 && CurrentTabCount == PrevCount)
                    s.Append("<br>");
                else
                {
                    while (CurrentTabCount < PrevCount)
                    {
                        divs.Pop();
                        close();
                        PrevCount = divs.Count > 0 ? divs.Peek() : 0;
                    }
                    if (CurrentTabCount > PrevCount)
                    {
                        s.Append("<div style=\"margin-left:");
                        s.Append(converter.TabIndent * (CurrentTabCount - PrevCount));
                        s.Append("pt\">");
                        divs.Push(CurrentTabCount);
                    }
                    PrevCount = CurrentTabCount;
                }
                newline = false;
            }

            void close()
            {
                s.Append("</div>");
                
            }
            public void Add(int count)
            {
                if (Counting) CurrentTabCount += count;
                else for (int t = count * converter.TabSpaces; t > 0; t--) s.Append(space);
            }

            public void CloseAll()
            {
                for (int i = 0; i < divs.Count; i++)
                {
                    close();
                }
                divs.Clear();
            }

        }
        #endregion

        StringBuilder s;

        #region Spaces
        /// <summary>
        /// The html representation of a space (non-breaking space). This is needed
        /// if more than one space is used subsequently
        /// </summary>
        const string space = "&nbsp";
        /// <summary>
        /// This amount of tabspaces at the start of a line will be replaced
        /// with a tab
        /// </summary>
        int TabSpaces;
        /// <summary>
        /// Read spaces (one space is used normally (' '), more than one as a collection of  \&nbsp )
        /// </summary>
        void ReadSpaces(int spacecount)
        {
            if (spacecount >= TabSpaces)
            {
                //replace with tab
                tab.Add(spacecount / TabSpaces);
                spacecount %= TabSpaces;
                if (spacecount == 0) return; //exit point!!                
            }

            tab.StopCount();
            if (spacecount == 1)
            {
                s.Append(" ");
            }
            else
            {
                //add the nbsp spacecount times
                for (; spacecount-- > 0; )
                    s.Append(space);
            }
        }
        #endregion

        #region Replacements

        private static readonly char[]
            ToReplace = ("<>\"&" + ((char)8216).ToString() + ((char)8217).ToString()).ToCharArray();
        private static readonly
            string[] ReplaceWith = { "&lt", "&gt", "&quot", "&amp", "'", "'" }; //indent-tabs can be used, other tabs are replaced by a space

        int replacementindex;
        bool CheckReplacements()
        {
            for (replacementindex = 0; replacementindex < ToReplace.Length; replacementindex++)
            {
                if (ToReplace[replacementindex] == txt[index])
                {
                    s.Append(ReplaceWith[replacementindex]);
                    return true;
                }
            }
            return false;
        }
        #endregion

        /// <summary>
        /// The actual start of reading in the rtf text
        /// </summary>
        public override StringBuilder Append(StringBuilder sb)
        {
            //(re)set values
            s = sb;
            tab = new Tabber(this);
            index = 0;

            htmltypes = HtmlType.GetHtmlTypes(s);

            for (index = 1; index < len; index++) //Start from second position, skip first "{", len is one less then length: skip last "}"
            {
                if (IgnoreList.IndexOf(txt[index]) != -1) continue;

                if (txt[index] == '\\')
                {//keyword?
                    if (@"\{}".IndexOf(txt[++index]) == -1) //check if the '\' is part of an escap sequence
                    {//no escape sequence->keyword
                        ReadKeyWord();
                        continue;
                    }
                }

                //check spaces
                int spacestart = index;
                while (txt[index] == ' ')
                    index++;
                if (index > spacestart)
                {
                    ReadSpaces(index-- - spacestart);
                    continue;
                }

                //indicate to the Tabber that text is starting and 
                //thus it now has to decide what to do with the indent
                tab.StopCount();

                //check if the current character has to be replaced
                if (CheckReplacements()) continue;
                
                //finally, if none of the above handled the current character, it can be added
                s.Append(txt[index]);
                
            }

            //close all open keyword tags
            CloseAllKeywords();
            //close all open divs
            tab.CloseAll();
            

            tab = null;
            htmltypes = null;
            return s;
        }
    }
    #endregion
}
